iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
自我挑戰組

Effective C++ 讀書筆記系列 第 24

[Day 24] Make interfaces easy to use correctly and hard to use incorrectly (1)

  • 分享至 

  • xImage
  •  

前言

終於進入第四章了!
第四章的主題是 "Designs and Declarations,是關於 設計與宣告好的C++介面的一些重點,最一開始是所有介面都要遵循的大重點,也是今天等等會介紹的守則;接下來則是針對各種方面的守則,包含 正確性、效率、封裝、維護、擴展性與慣例遵循等等。當然要注意的事情無限多,這些守則就是針對最重要的觀念做介紹,列出一些常見錯誤,以及在 class, function跟template設計中常見的問題與解法。
馬上來看看重中之重─第四章的第一條守則吧!

理想目標: 正確才能動

今天的守則是:

Make interfaces easy to use correctly and hard to use incorrectly

這就是設計interface的 大原則。無論是function interface, class interface還是template interface,我們都應該以這個為目標。所謂interface就是我們的code與client/ user互動的入口,而使用者應該都想要 正確地使用你的interface,如果它用錯了,那這個interface的設計者就難辭其咎了,所以我們的設計應該要讓使用者難以用錯,最最理想的目標就是─如果用錯了就不能編譯,如果能編譯那行為就應該符合預期;再用比較少的字來描述─ 能動=正確;不正確=不能動。這樣user想錯用也用不了,直接被compiler把關,能動的就是對的結果。

user可能會怎麼用錯?

而上面寫的是理想的方向,而現在我們來具體的看看要怎麼達成這種設計。要做出這種設計,首先需要思考的就是 user可能會怎麼錯用。例如,我們設計了一個class來代表時間:

class Date
{
public:
    Date(int month, int day, int year);
}

可以想像到user用的時候可能會犯兩種錯:
1. 傳錯順序:例如把月份與年份弄反。

Date d(30, 3, 1995);

2. 不合理的數值:例如弄出2月30日。

Date d(2, 30, 1995);

下一步就是想辦法讓這些情況,可以被偵測出來並不給使用。

創建new type控管

很多可能會出現的error都可以藉由我們自訂的new type來避免。
例如在上面這個例子,我們可以用簡單的 wrapper type 來規範年月日,並在Date的constructor裡面使用它們。

struct Day
{
    explicit Day(int d): val(d){}
    int val;
};

struct Month
{
    explicit Month(int m): val(m){}
    int val;
};

struct Year
{
    explicit Day(int y): val(y){}
    int val;
};

class Date
{
public:
    Date(const Month& m, const Day& d, const Year& y);
};

 Date d(30, 3, 1995);  // error! wrong types, require Month, Day, Year
 Date d(Day(30), Month(3), Year(1995)); // error! wrong types, require Month, Day, Year
 Date d(Month(3), Day(30), Year(1995)); // correct

這樣就能初步過濾掉第一種傳錯順序的情形,也避免掉其他可能錯用到別的type的狀況。而要避免掉第二種不合理的值,我們也有幾種方式可以對他們加以設計,例如用 enum 來列出所有可能的值;然而,enum有其他type-safe問題─C++11之後建議改成 enum class 來使用避免這個問題─這個我們先略過,enum/ enum class的使用可參考其他參考資料,這邊我們先不用enum,用相對安全的方式,來將所有合理的Month值列出:

class Month
{
public:
    static Month Jan() { return Month(1); }
    static Month Feb() { return Month(2); }
    ...
private:
    explicit Mont(int m);
};

Date d(Month::Mar(), Day(30), Year(1995));

這就是使用Month() function取代原本的object的方式。

一致性很重要!

還有一個避免誤用的方式就是針對new type限制它可做的行為,例如─加上 const。例如我們在前面的守則有講過的([Day 5] Use const whenever possible (1)),在operator*return type加上const可以避免像下面這種誤用的行為:

if(a*b=c)... // error!

從上面這個可以延伸出另外一個守則─ 讓你的types行為跟built-in type一致。因為user已經熟知built-in type是怎麼使用的,所以最好讓你的type也一樣,這樣user就自然而然地知道該怎麼用。像上面的例子,假設有int aint b,指派值給a*b是不合法的行為,所以除非有特殊理由,最好也讓自己定義的type也遵循一樣的規則。這個 consistency─一致性是其中的精髓。就像STL的容器們有大量的共通規則,就讓它們變得比較容易使用,例如所有STL容器都有size()這個member function。

明天,我們繼續來看一致性的實例,以及為什麼比較少的特殊規則可以讓interface變得容易使用,結束這個守則。

參考資料


上一篇
[Day 23] Store new objects in smart pointers in standalone statements
下一篇
[Day 25] Make interfaces easy to use correctly and hard to use incorrectly (2)
系列文
Effective C++ 讀書筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言